package phantm.phases

import phantm.cfg.LabeledDirectedGraphImp
import phantm.cfg.Trees._
import phantm.cfg.EdgeImp
import phantm.cfg.VertexImp
import phantm.cfg.ControlFlowGraph
import phantm.util.{ PhasesUtils, Reporter }
import phantm.symbols._
import phantm.Settings
import phantm.types._

object CFGInlinePhase extends Phase {
  def name = "Inlining call graphs"
  def description = "Inlining the call graphs for each method in the main call graph"

  def run(ctx: PhasesContext): PhasesContext = {
    val generator = SingleGraphGenerator(ctx)
    val inliner = generator.GraphsInliner
    inliner.joinCFGs
    if (!Settings.get.exportPaths.exportICFGPath.isEmpty) {
      inliner.writeDottyToFile(Settings.get.exportPaths.exportICFGPath.get, "Inlined Control Flow Graph")
    }
    ctx
  }
}

case class SingleGraphGenerator(ctx: PhasesContext) {

  object GraphsInliner extends LabeledDirectedGraphImp[Statement] {
    val helper = new PhasesUtils(ctx)
    var cfg = helper.getCFG(None).get
    var edges = cfg.E
    val entry = cfg.entry
    var visited: Set[EdgeImp[Statement]] = Set()

    def joinCFGs = {

      def inlineCFG(variable: Variable, optionMethod: Option[MethodSymbol], params: List[SimpleValue], edge: EdgeImp[Statement]): Unit = {
        optionMethod match {
          case someMethod @ Some(method) =>
            helper.getCFG(someMethod) match {
              case Some(cfgToInline) =>
                var fromVertex = edge.v1
                val oldCfg = cfg
                val oldEnvironment = ctx.cfgsWithTypes(cfg)

                //handle parameters
                var argsList = method.argList
                for (param <- params) {
                  val aVertex = newVertex
                  val arg = argsList.head
                  val variable = Identifier(new VariableSymbol(arg._1))
                  cfg += (fromVertex, Assign(variable, param), aVertex)
                  argsList = argsList.tail
                  fromVertex = aVertex
                }

                //join on entry
                for (firstEdge <- cfgToInline.entry.out) { if (firstEdge.v2 != cfgToInline.exit) { cfg += (fromVertex, firstEdge.lab, firstEdge.v2) } }

                //join on exit
                val lastEdges = cfgToInline.exit.in //there might be more return statements
                var values: Set[SimpleValue] = Set()
                val aVertex = newVertex
                var propagatedEnv: TypeEnvironment = BaseTypeEnvironment
                for (lastEdge <- lastEdges) {
                  lastEdge.lab match {
                    case Return(expression) =>
                      values += expression
                      val assign = new Assign(variable, expression)
                      if (lastEdge.v1 != cfgToInline.entry) {
                        cfg += (lastEdge.v1, assign, aVertex)
                        ctx.cfgsWithTypes(cfgToInline).get(lastEdge.v1) match {
                          case Some(env) => propagatedEnv = propagatedEnv union env
                          case None =>
                        }
                      } else {
                        cfg += (fromVertex, assign, aVertex)
                        ctx.cfgsWithTypes(cfgToInline).get(fromVertex) match {
                          case Some(env) => propagatedEnv = propagatedEnv union env
                          case None =>
                        }
                      }
                    case throwStatement @ Throw(_) => cfg += (lastEdge.v1, throwStatement, cfg.exit)
                    case _ =>
                  }
                }
                cfg += (aVertex, new Assign(variable, UnionValue(values)), edge.v2)
                cfg -= (edge.v1, edge.lab, edge.v2)
                cfg.joinGraphsWithoutEntryAndExit(cfgToInline)
                ctx.cfgsWithTypes = ctx.cfgsWithTypes - oldCfg

                //copy the environment
                var env = ctx.cfgsWithTypes(cfgToInline).filter(vertex => vertex != cfgToInline.entry && vertex != cfgToInline.exit)
                env = env + (aVertex -> propagatedEnv)
                ctx.cfgsWithTypes = ctx.cfgsWithTypes updated (cfg, oldEnvironment ++ env)
                edges = edges ++ cfg.E
              case None =>
            }
          case None =>
        }
      }

      while (!edges.isEmpty) {
        val edge = edges.head
        if (!(visited contains edge)) {
          val fromVertex = edge.v1
          val vertexEnvironmentMap = ctx.cfgsWithTypes(cfg)
          val environment = vertexEnvironmentMap(fromVertex)
          edge.lab match {
            case Assign(variable, MethodCall(receiver, methodId, parameters)) => inlineCFG(variable, helper.lookupMethod(helper.classFromSimpleValue(receiver, environment), methodId), parameters, edge)
            case Assign(variable, StaticMethodCall(classRef, methodId, parameters)) => classRef match {
              case ClassRefFixed(classSymbol) => inlineCFG(variable, helper.lookupMethod(Some(classSymbol), methodId), parameters, edge)
              case _ =>
            }
            case Assign(variable, New(classRef, parameters)) => classRef match {
              case ClassRefFixed(classSymbol) => inlineCFG(variable, helper.lookupConstructor(classSymbol), parameters, edge)
              case _ =>
            }
            case _ =>
          }
          visited += edge
        }
        edges = edges - edge
      }

      for (edge <- cfg.E) {
        this += (edge.v1, edge.lab, edge.v2)
      }
      val env = ctx.cfgsWithTypes(cfg)
      Reporter.get.clear
      val tfa = new TypeFlowAnalyzer(cfg, ctx.globalSymbols, ctx, collectGlobals = true, facts = Some(env))
      val results = tfa.analyze
    }
  }
}
